namespace Trader.Trading
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using BO.Interface;
    using Interface;

    public class Portfolio : IPortfolio
    {
        #region Private Member's

        private readonly ICommissionCharger commissionCharger;
        private readonly IMoney currentCash;
        private readonly Dictionary<DateTime, IPortfolioState> datesStates;
        private readonly PortfolioState defualtState;
        private readonly IMoney initialMoney;
        private readonly IList<IPortfolioMember> portfolioMembers;
        private readonly IList<ITradeAction> tradeActions;

        public IPortfolioState this[DateTime date]
        {
            get
            {
                IPortfolioState portfolioState;
                if (!datesStates.TryGetValue(date, out portfolioState))
                {
                    if (!datesStates.Keys.Where(keyDate => keyDate < date).Any())
                    {
                        portfolioState = defualtState;
                    }
                    else
                    {
                        portfolioState = datesStates[datesStates.Keys.Where(keyDate => keyDate < date).Min()];
                    }
                }
                return portfolioState;
            }
        }

        #endregion

        #region Const'

        public Portfolio(IMoney initialCash, ICommissionCharger commissionCharger)
        {
            initialMoney = initialCash.Clone();
            currentCash = initialCash.Clone();
            portfolioMembers = new List<IPortfolioMember>();
            tradeActions = new List<ITradeAction>();
            datesStates = new Dictionary<DateTime, IPortfolioState>();
            defualtState = new PortfolioState(initialMoney, new List<IPortfolioMember>());
            this.commissionCharger = commissionCharger;
        }

        public void UpdateUnits(DateTime date)
        {
            bool hadUpdate = false;
            foreach (IPortfolioMember member in portfolioMembers)
            {
                hadUpdate = member.UpdateUnits(date) || hadUpdate;
            }

            if (hadUpdate)
            {
                AddState(date);
            }
        }

        public void Sell(IEnumerable<IPortfolioMember> membersToSell, DateTime date)
        {
            foreach (IPortfolioMember member in membersToSell)
            {
                Sell(member, date);
            }

            AddState(date);
        }

        #endregion

        #region IPortfolio Members

        public IList<IPortfolioMember> PortfolioMembers
        {
            get { return portfolioMembers; }
        }

        public void RearrangeUsingRecomendations(IList<IRecommendation> recomendations, DateTime currentTradingDate)
        {
            if (!recomendations.Any())
            {
                return;
            }

            if (!datesStates.ContainsKey(currentTradingDate))
            {
                return;
            }

            IEnumerable<IPortfolioMember> membersOutOfRecommendations =
                portfolioMembers.Where(member => !recomendations.Contains(member.Recommendation));
            List<IPortfolioMember> sellList = membersOutOfRecommendations.ToList();
            foreach (IPortfolioMember portfolioMember in sellList)
            {
                Sell(portfolioMember, currentTradingDate);
            }

            decimal totalWeight =
                recomendations.Sum(recomendation => recomendation.GetCurrentChangePerDay(currentTradingDate).Value);

            IMoney totalMoney = CalculateTotalPortfolioValue(currentTradingDate);

            IEnumerable<IRecommendation> recommendationsOnStocksInPortfolio =
                portfolioMembers.Where(member => recomendations.Contains(member.Recommendation))
                    .Select(member => member.Recommendation);

            foreach (IRecommendation recomendation in recommendationsOnStocksInPortfolio)
            {
                IPortfolioMember portfolioMember = portfolioMembers.First(member => member.Stock == recomendation.Stock);
                int newUnits =
                    portfolioMember.Stock.CalulateUnitsInMoney(
                        (recomendation.GetCurrentChangePerDay(currentTradingDate).Value/totalWeight)*totalMoney,
                        currentTradingDate);
                if (newUnits != portfolioMember.Units(currentTradingDate))
                {
                    if (newUnits < portfolioMember.Units(currentTradingDate))
                    {
                        Reduce(portfolioMember, currentTradingDate, newUnits);
                    }
                    else
                    {
                        Raise(portfolioMember, currentTradingDate, newUnits);
                    }
                }
            }

            IEnumerable<IRecommendation> recommendationsOfNewStocks =
                recomendations.Where(
                    recomendation => !portfolioMembers.Where(member => member.Stock == recomendation.Stock).Any());

            IMoney totalMonetReducedCommission = totalMoney;
            totalMonetReducedCommission.Multiply(0.995M);
            foreach (IRecommendation recomendation in recommendationsOfNewStocks)
            {
                int units = recomendation.Stock.CalulateUnitsInMoney(
                    (recomendation.GetCurrentChangePerDay(currentTradingDate).Value/totalWeight)*
                    totalMonetReducedCommission,
                    currentTradingDate);
                Buy(recomendation, currentTradingDate, units);
            }

            AddState(currentTradingDate);
        }

        public void SellAll(DateTime date)
        {
            while (portfolioMembers.Any())
            {
                Sell(portfolioMembers.First(), date);
            }
            AddState(date);
        }

        #endregion

        private IMoney CalculateTotalPortfolioValue(DateTime currentTradingDate)
        {
            IMoney totalMoney = currentCash.Clone();
            foreach (IPortfolioMember portfolioMember in portfolioMembers)
            {
                totalMoney.Add(portfolioMember.CalculateValue(currentTradingDate), currentTradingDate);
            }
            return totalMoney;
        }

        public void Sell(IPortfolioMember member, DateTime currentTradingDay)
        {
            var tradeAction = new SellAction(member.Stock, currentTradingDay, member.Units(currentTradingDay));
            tradeActions.Add(tradeAction);
            currentCash.Substract(commissionCharger.Charge(tradeAction, currentTradingDay), currentTradingDay);
            currentCash.Add(member.CalculateValue(currentTradingDay), currentTradingDay);
            member.SetSellDate(currentTradingDay);
            portfolioMembers.Remove(member);
        }

        private void AddState(DateTime date)
        {
            PortfolioState portfolioState = CreatePortfolioState(date);
            datesStates[date] = portfolioState;
        }

        private PortfolioState CreatePortfolioState(DateTime valueDate)
        {
            /*IList<IPortfolioMember> currentPortfolioMembers = portfolioMembers.Select(
                member =>
                (IPortfolioMember)
                new PortfolioMember(member.Recommendation, member.BuyDate, member.Units(valueDate))).ToList();*/
            IList<IPortfolioMember> currentPortfolioMembers = portfolioMembers.ToList();
            return new PortfolioState(currentCash, currentPortfolioMembers);
        }

        private void Buy(IRecommendation recommendation, DateTime currentTradingDay, int units)
        {
            if (units == 0)
            {
                return;
            }

            var tradeAction = new BuyAction(recommendation.Stock, currentTradingDay, units);
            tradeActions.Add(tradeAction);
            currentCash.Substract(commissionCharger.Charge(tradeAction, currentTradingDay), currentTradingDay);
            currentCash.Substract(recommendation.Stock.AsMoney(units, currentTradingDay), currentTradingDay);
            portfolioMembers.Add(new PortfolioMember(recommendation, currentTradingDay, units));
        }

        private void Raise(IPortfolioMember portfolioMember, DateTime currentTradingDay, int newUnits)
        {
            int unitsToBuy = newUnits - portfolioMember.Units(currentTradingDay);
            var tradeAction = new BuyAction(portfolioMember.Stock, currentTradingDay, unitsToBuy);
            tradeActions.Add(tradeAction);
            currentCash.Substract(commissionCharger.Charge(tradeAction, currentTradingDay), currentTradingDay);
            portfolioMember.AdjustUnits(newUnits, currentTradingDay);
            currentCash.Substract(portfolioMember.Stock.AsMoney(unitsToBuy, currentTradingDay), currentTradingDay);
        }

        private void Reduce(IPortfolioMember portfolioMember, DateTime currentTradingDay, int newUnits)
        {
            if (newUnits == 0)
            {
                Sell(portfolioMember, currentTradingDay);
                return;
            }

            int unitsToSell = portfolioMember.Units(currentTradingDay) - newUnits;
            var tradeAction = new SellAction(portfolioMember.Stock, currentTradingDay, unitsToSell);
            tradeActions.Add(tradeAction);
            currentCash.Substract(commissionCharger.Charge(tradeAction, currentTradingDay), currentTradingDay);
            portfolioMember.AdjustUnits(newUnits, currentTradingDay);
            currentCash.Add(portfolioMember.Stock.AsMoney(unitsToSell, currentTradingDay), currentTradingDay);
        }

        #region Nested type: PortfolioState

        private class PortfolioState : IPortfolioState
        {
            private readonly IMoney money;
            private readonly IList<IPortfolioMember> portfolioMembers;

            public PortfolioState(IMoney money, IEnumerable<IPortfolioMember> portfolioMembers)
            {
                this.money = money.Clone();
                this.portfolioMembers = portfolioMembers.ToList().AsReadOnly();
            }

            #region IPortfolioState Members

            public IList<IPortfolioMember> PortfolioMembers
            {
                get { return portfolioMembers; }
            }

            public IMoney Cash
            {
                get { return money; }
            }

            public IMoney GetValue(DateTime date)
            {
                IMoney value = money.Clone();
                foreach (IPortfolioMember member in portfolioMembers)
                {
                    value.Add(member.Stock.AsMoney(member.Units(date), date), date);
                }
                return value;
            }

            #endregion
        }

        #endregion
    }
}